// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Text;

namespace System.Net
{
    public sealed partial class HttpListenerResponse : IDisposable
    {
        private BoundaryType _boundaryType = BoundaryType.None;
        private CookieCollection? _cookies;
        private readonly HttpListenerContext? _httpContext;
        private bool _keepAlive = true;
        private HttpResponseStream? _responseStream;
        private string? _statusDescription;
        private WebHeaderCollection _webHeaders = new WebHeaderCollection();

        public WebHeaderCollection Headers
        {
            get => _webHeaders;
            set
            {
                _webHeaders = new WebHeaderCollection();
                foreach (string headerName in value.AllKeys)
                {
                    _webHeaders.Add(headerName, value[headerName]);
                }
            }
        }

        public Encoding? ContentEncoding { get; set; }

        public string? ContentType
        {
            get => Headers[HttpKnownHeaderNames.ContentType];
            set
            {
                CheckDisposed();
                if (string.IsNullOrEmpty(value))
                {
                    Headers.Remove(HttpKnownHeaderNames.ContentType);
                }
                else
                {
                    Headers.Set(HttpKnownHeaderNames.ContentType, value);
                }
            }
        }

        private HttpListenerContext HttpListenerContext => _httpContext!;

        private HttpListenerRequest HttpListenerRequest => HttpListenerContext!.Request;

        internal EntitySendFormat EntitySendFormat
        {
            get => (EntitySendFormat)_boundaryType;
            set
            {
                CheckDisposed();
                CheckSentHeaders();
                if (value == EntitySendFormat.Chunked && HttpListenerRequest.ProtocolVersion.Minor == 0)
                {
                    throw new ProtocolViolationException(SR.net_nochunkuploadonhttp10);
                }
                _boundaryType = (BoundaryType)value;
                if (value != EntitySendFormat.ContentLength)
                {
                    _contentLength = -1;
                }
            }
        }

        public bool SendChunked
        {
            get => EntitySendFormat == EntitySendFormat.Chunked;
            set => EntitySendFormat = value ? EntitySendFormat.Chunked : EntitySendFormat.ContentLength;
        }



        private static bool CanSendResponseBody(int responseCode) =>
            // We MUST NOT send message-body when we send responses with these Status codes
            responseCode is not (100 or 101 or 204 or 205 or 304);

        public long ContentLength64
        {
            get => _contentLength;
            set
            {
                CheckDisposed();
                CheckSentHeaders();
                ArgumentOutOfRangeException.ThrowIfNegative(value);
                _contentLength = value;
                _boundaryType = BoundaryType.ContentLength;
            }
        }

        public CookieCollection Cookies
        {
            get => _cookies ??= new CookieCollection();
            set => _cookies = value;
        }

        public bool KeepAlive
        {
            get => _keepAlive;
            set
            {
                CheckDisposed();
                _keepAlive = value;
            }
        }

        public Stream OutputStream
        {
            get
            {
                CheckDisposed();
                EnsureResponseStream();
                return _responseStream!;
            }
        }

        public string? RedirectLocation
        {
            get => Headers[HttpResponseHeader.Location];
            set
            {
                // note that this doesn't set the status code to a redirect one
                CheckDisposed();
                if (string.IsNullOrEmpty(value))
                {
                    Headers.Remove(HttpKnownHeaderNames.Location);
                }
                else
                {
                    Headers.Set(HttpKnownHeaderNames.Location, value);
                }
            }
        }

        public string StatusDescription
        {
            get
            {
                // if the user hasn't set this, generate on the fly, if possible.
                // We know this one is safe, no need to verify it as in the setter.
                return _statusDescription ??= HttpStatusDescription.Get(StatusCode) ?? string.Empty;
            }
            set
            {
                CheckDisposed();
                ArgumentNullException.ThrowIfNull(value);

                // Need to verify the status description doesn't contain any control characters except HT.  We mask off the high
                // byte since that's how it's encoded.
                for (int i = 0; i < value.Length; i++)
                {
                    char c = (char)(0x000000ff & (uint)value[i]);
                    if ((c <= 31 && c != (byte)'\t') || c == 127)
                    {
                        throw new ArgumentException(SR.net_WebHeaderInvalidControlChars, nameof(value));
                    }
                }

                _statusDescription = value;
            }
        }

        public void AddHeader(string name, string value)
        {
            if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"name={name}, value={value}");
            Headers.Set(name, value);
        }

        public void AppendHeader(string name, string value)
        {
            if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"name={name}, value={value}");
            Headers.Add(name, value);
        }

        public void AppendCookie(Cookie cookie)
        {
            ArgumentNullException.ThrowIfNull(cookie);

            if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"cookie: {cookie}");
            Cookies.Add(cookie);
        }

        private void ComputeCookies()
        {
            if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"Entering Set-Cookie: {Headers[HttpResponseHeader.SetCookie]}, Set-Cookie2: {Headers[HttpKnownHeaderNames.SetCookie2]}");

            if (_cookies != null)
            {
                // now go through the collection, and concatenate all the cookies in per-variant strings
                string? setCookie2 = null, setCookie = null;
                for (int index = 0; index < _cookies.Count; index++)
                {
                    Cookie cookie = _cookies[index];
                    string cookieString = cookie.ToServerString();
                    if (string.IsNullOrEmpty(cookieString))
                    {
                        continue;
                    }
                    if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"Now looking at index:{index} cookie: {cookie}");
                    if (cookie.IsRfc2965Variant())
                    {
                        setCookie2 = setCookie2 == null ? cookieString : setCookie2 + ", " + cookieString;
                    }
                    else
                    {
                        setCookie = setCookie == null ? cookieString : setCookie + ", " + cookieString;
                    }
                }

                if (!string.IsNullOrEmpty(setCookie))
                {
                    Headers.Set(HttpKnownHeaderNames.SetCookie, setCookie);
                    if (string.IsNullOrEmpty(setCookie2))
                    {
                        Headers.Remove(HttpKnownHeaderNames.SetCookie2);
                    }
                }

                if (!string.IsNullOrEmpty(setCookie2))
                {
                    Headers.Set(HttpKnownHeaderNames.SetCookie2, setCookie2);
                    if (string.IsNullOrEmpty(setCookie))
                    {
                        Headers.Remove(HttpKnownHeaderNames.SetCookie);
                    }
                }
            }

            if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"Exiting Set-Cookie: {Headers[HttpResponseHeader.SetCookie]} Set-Cookie2: {Headers[HttpKnownHeaderNames.SetCookie2]}");
        }

        public void Redirect([StringSyntax(StringSyntaxAttribute.Uri)] string url)
        {
            if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"url={url}");
            Headers[HttpResponseHeader.Location] = url;
            StatusCode = (int)HttpStatusCode.Redirect;
            StatusDescription = HttpStatusDescription.Get(StatusCode)!;
        }

        public void SetCookie(Cookie cookie)
        {
            ArgumentNullException.ThrowIfNull(cookie);

            Cookie newCookie = cookie.Clone();
            int added = Cookies.InternalAdd(newCookie, true);

            if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"cookie: {cookie}");

            if (added != 1)
            {
                // The Cookie already existed and couldn't be replaced.
                throw new ArgumentException(SR.net_cookie_exists, nameof(cookie));
            }
        }

        void IDisposable.Dispose() => Dispose();

        private void CheckDisposed()
        {
            ObjectDisposedException.ThrowIf(Disposed, this);
        }

        private void CheckSentHeaders()
        {
            if (SentHeaders)
            {
                throw new InvalidOperationException(SR.net_rspsubmitted);
            }
        }
    }
}
